package glapp;

import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.nio.*;
import java.io.*;
import java.net.URL;
import org.lwjgl.opengl.*;
import org.lwjgl.util.glu.*;
import ui.Message;

/**
 * Collection of functions to init and run an OpenGL app using LWJGL.
 * <P>
 * Includes functions to handle: <BR>
 * Setup display mode, keyboard, mouse, handle events<BR>
 * Run main loop of application <BR>
 * Buffer allocation -- manage IntBuffer, ByteBuffer calls. <BR>
 * OpenGL functions -- convert screen/world coords, set modes, lights, etc. <BR>
 * Utility functions -- load images, convert pixels, getTimeInMillis, etc. <BR>
 * <P>
 * Has a main() function to run as an application, though this class has only
 * minimal placeholder functionality. It is meant to be subclassed, and the
 * subclass will define setup() draw() mouseMove() functions, etc.
 * <P>
 * GLApp initializes the LWJGL environment for OpenGL rendering, ie. creates a
 * window, sets the display mode, inits mouse and keyboard, then runs a loop.
 * <P>
 * Uses GLImage to load and hold image pixels.
 * <P>
 * napier -at- potatoland -dot- org
 * 
 * @see GLImage
 */
public class GLApp
{
	// Byte size of data types: Used when allocating native buffers
	private static final int				SIZE_FLOAT				= 4;
	private static final int				SIZE_INT				= 4;
	private static FloatBuffer			tmpFloats				= allocFloats(4);				// temp var used by setLightPos(), setFog()
	public static final float			PIUNDER180				= 57.2957795130f;				// A constant used in navigation: 180/PI;
	private static Hashtable<String, String>	OpenGLextensions;										// will be populated by extensionExists()

	
	/**
	 * Allocate a texture (glGenTextures) and return the handle to it.
	 */
	private static int allocateTexture()
	{
		IntBuffer textureHandle = allocInts(1);
		GL11.glGenTextures(textureHandle);
		return textureHandle.get(0);
	}

	/**
	 * Create a texture and mipmap from the given image file.
	 */
	public static int makeTexture(String textureImagePath)
	{
		int textureHandle = 0;
		GLImage textureImg = loadImage(textureImagePath);
		if (textureImg != null)
		{
			textureHandle = makeTexture(textureImg);
			makeTextureMipMap(textureHandle, textureImg);
		}
		return textureHandle;
	}

	/**
	 * Create a texture from the given image.
	 */
	private static int makeTexture(GLImage textureImg)
	{
		if (textureImg != null)
		{
			if (isPowerOf2(textureImg.w) && isPowerOf2(textureImg.h))
			{
				return makeTexture(textureImg.pixelBuffer, textureImg.w, textureImg.h, false);
			}
			else
			{
				Message.getInstance().msg("GLApp.makeTexture(GLImage) Warning: not a power of two: " + textureImg.w + "," + textureImg.h);
				textureImg.convertToPowerOf2();
				return makeTexture(textureImg.pixelBuffer, textureImg.w, textureImg.h, false);
			}
		}
		return 0;
	}

	/**
	 * Returns true if n is a power of 2. If n is 0 return zero.
	 */
	private static boolean isPowerOf2(int n)
	{
		if (n == 0)
		{
			return false;
		}
		return (n & (n - 1)) == 0;
	}

	/**
	 * Create a texture from the given pixels in the default OpenGL RGBA pixel
	 * format. Configure the texture to repeat in both directions and use LINEAR
	 * for magnification.
	 * <P>
	 * 
	 * @return the texture handle
	 */
	private static int makeTexture(ByteBuffer pixels, int w, int h, boolean anisotropic)
	{
		// get a new empty texture
		int textureHandle = allocateTexture();
		// preserve currently bound texture, so glBindTexture() below won't affect anything)
		GL11.glPushAttrib(GL11.GL_TEXTURE_BIT);
		// 'select' the new texture by it's handle
		GL11.glBindTexture(GL11.GL_TEXTURE_2D, textureHandle);
		// set texture parameters
		GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_S, GL11.GL_REPEAT);
		GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_WRAP_T, GL11.GL_REPEAT);
		GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MAG_FILTER, GL11.GL_LINEAR); //GL11.GL_NEAREST);
		GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR); //GL11.GL_NEAREST);

		// make texture "anisotropic" so it will minify more gracefully
		if (anisotropic && extensionExists("GL_EXT_texture_filter_anisotropic"))
		{
			// Due to LWJGL buffer check, you can't use smaller sized buffers (min_size = 16 for glGetFloat()).
			FloatBuffer max_a = allocFloats(16);
			// Grab the maximum anisotropic filter.
			GL11.glGetFloat(EXTTextureFilterAnisotropic.GL_MAX_TEXTURE_MAX_ANISOTROPY_EXT, max_a);
			// Set up the anisotropic filter.
			GL11.glTexParameterf(GL11.GL_TEXTURE_2D, EXTTextureFilterAnisotropic.GL_TEXTURE_MAX_ANISOTROPY_EXT, max_a.get(0));
		}

		// Create the texture from pixels
		GL11.glTexImage2D(GL11.GL_TEXTURE_2D, 0, // level of detail
				GL11.GL_RGBA8, // internal format for texture is RGB with Alpha
				w, h, // size of texture image
				0, // no border
				GL11.GL_RGBA, // incoming pixel format: 4 bytes in RGBA order
				GL11.GL_UNSIGNED_BYTE, // incoming pixel data type: unsigned bytes
				pixels); // incoming pixels

		// restore previous texture settings
		GL11.glPopAttrib();

		return textureHandle;
	}

	/**
	 * Build Mipmaps for currently bound texture (builds a set of textures at
	 * various levels of detail so that texture will scale up and down
	 * gracefully)
	 * 
	 * @param textureImg
	 *            the texture image
	 * @return error code of buildMipMap call
	 */
	private static int makeTextureMipMap(int textureHandle, GLImage textureImg)
	{
		int ret = 0;
		if (textureImg != null && textureImg.isLoaded())
		{
			GL11.glBindTexture(GL11.GL_TEXTURE_2D, textureHandle);
			ret = GLU.gluBuild2DMipmaps(GL11.GL_TEXTURE_2D, GL11.GL_RGBA8, textureImg.w, textureImg.h, GL11.GL_RGBA, GL11.GL_UNSIGNED_BYTE, textureImg.getPixelBytes());
			if (ret != 0)
			{
				Message.getInstance().err("GLApp.makeTextureMipMap(): Error occured while building mip map, ret=" + ret + " error=" + GLU.gluErrorString(ret));
			}
			// Assign the mip map levels and texture info
			GL11.glTexParameteri(GL11.GL_TEXTURE_2D, GL11.GL_TEXTURE_MIN_FILTER, GL11.GL_LINEAR_MIPMAP_NEAREST);
			GL11.glTexEnvf(GL11.GL_TEXTURE_ENV, GL11.GL_TEXTURE_ENV_MODE, GL11.GL_MODULATE);
		}
		return ret;
	}

	/**
	 * Find a power of two equal to or greater than the given value. Ie.
	 * getPowerOfTwoBiggerThan(800) will return 1024.
	 * <P>
	 * 
	 * @see makeTextureForScreen()
	 * @param dimension
	 * @return a power of two equal to or bigger than the given dimension
	 */
	protected static int getPowerOfTwoBiggerThan(int n)
	{
		if (n < 0)
			return 0;
		--n;
		n |= n >> 1;
		n |= n >> 2;
		n |= n >> 4;
		n |= n >> 8;
		n |= n >> 16;
		return n + 1;
	}

	/**
	 * Set the position (or direction) of a light to the given xyz.
	 */
	public static void setLightPosition(int GLLightHandle, float[] position)
	{
		put(tmpFloats, position);
		GL11.glLight(GLLightHandle, GL11.GL_POSITION, tmpFloats);
	}

	/**
	 * Load an image from the given file and return a GLImage object.
	 * 
	 * @param image
	 *            filename
	 * @return the loaded GLImage
	 */
	private static GLImage loadImage(String imgFilename)
	{
		GLImage img = new GLImage(imgFilename);
		if (img.isLoaded())
		{
			return img;
		}
		return null;
	}

	private static IntBuffer allocInts(int howmany)
	{
		return ByteBuffer.allocateDirect(howmany * SIZE_INT).order(ByteOrder.nativeOrder()).asIntBuffer();
	}

	private static FloatBuffer allocFloats(int howmany)
	{
		return ByteBuffer.allocateDirect(howmany * SIZE_FLOAT).order(ByteOrder.nativeOrder()).asFloatBuffer();
	}

	public static FloatBuffer allocFloats(float[] floatarray)
	{
		FloatBuffer fb = ByteBuffer.allocateDirect(floatarray.length * SIZE_FLOAT).order(ByteOrder.nativeOrder()).asFloatBuffer();
		fb.put(floatarray).flip();
		return fb;
	}

	private static void put(FloatBuffer b, float[] values)
	{
		b.clear();
		b.put(values).flip();
	}

	//========================================================================
	// Misc functions
	//========================================================================
	private static URL	appletBaseURL	= null;
	private static Class	rootClass		= GLApp.class;

	/**
	 * Open the given file and return the InputStream. This function assumes 1)
	 * that we're running an application and the file is in the local
	 * filesystem. If not found, then assume 2) we're in a jar file and look for
	 * the file in the current jar. If not found, then assume 3) we're running
	 * an applet and look for the file relative to the applet code base.
	 * 
	 * @param filename
	 *            to open
	 */
	public static InputStream getInputStream(String filename)
	{
		InputStream in = null;

		// 1) look for file in local filesystem
		try
		{
			in = new FileInputStream(filename);
		}
		catch (IOException ioe)
		{
			Message.getInstance().msg("GLApp.getInputStream (" + filename + "): " + ioe);
			if (in != null)
			{
				try
				{
					in.close();
				}
				catch (Exception e)
				{}
				in = null;
			}
		}
		catch (Exception e)
		{
			Message.getInstance().msg("GLApp.getInputStream (" + filename + "): " + e);
		}

		// 2) if couldn't open file, look in jar
		if (in == null && rootClass != null)
		{
			// NOTE: class.getResource() looks for files relative to the folder that the class is in.
			// ideally the class will be an application in the root of the installation, see setRootClass().
			URL u = null;
			if (filename.startsWith("."))
			{ // remove leading . ie. "./file"
				filename = filename.substring(1);
			}
			try
			{
				u = rootClass.getResource(filename);
			}
			catch (Exception ue)
			{
				Message.getInstance().msg("GLApp.getInputStream(): Can't find resource: " + ue);
			}
			//msg("GLApp.getInputStream (" +filename+ "): try jar resource url=" + u);
			if (u != null)
			{
				try
				{
					in = u.openStream();
				}
				catch (Exception e)
				{
					Message.getInstance().msg("GLApp.getInputStream (" + filename + "): Can't load from jar: " + e);
				}
			}

			// 3) try loading file from applet base url
			if (in == null && appletBaseURL != null)
			{
				try
				{
					u = new URL(appletBaseURL, filename);
				}
				catch (Exception ue)
				{
					Message.getInstance().msg("GLApp.getInputStream(): Can't make applet base url: " + ue);
				}
				//msg("GLApp.getInputStream (" +filename+ "): try applet base url=" + u);
				try
				{
					in = u.openStream();
				}
				catch (Exception e)
				{
					Message.getInstance().msg("GLApp.getInputStream (" + filename + "): Can't load from applet base URL: " + e);
				}
			}
		}
		return in;
	}

	/**
	 * Return a String array containing the path portion of a filename
	 * (result[0]), and the fileame (result[1]). If there is no path, then
	 * result[0] will be "" and result[1] will be the full filename.
	 */
	public static String[] getPathAndFile(String filename)
	{
		String[] pathAndFile = new String[2];
		Matcher matcher = Pattern.compile("^.*/").matcher(filename);
		if (matcher.find())
		{
			pathAndFile[0] = matcher.group();
			pathAndFile[1] = filename.substring(matcher.end());
		}
		else
		{
			pathAndFile[0] = "";
			pathAndFile[1] = filename;
		}
		return pathAndFile;
	}

	/**
	 * Return true if the OpenGL context supports the given OpenGL extension.
	 */
	private static boolean extensionExists(String extensionName)
	{
		if (OpenGLextensions == null)
		{
			String[] GLExtensions = GL11.glGetString(GL11.GL_EXTENSIONS).split(" ");
			OpenGLextensions = new Hashtable<String, String>();
			for (int i = 0; i < GLExtensions.length; i++)
			{
				OpenGLextensions.put(GLExtensions[i].toUpperCase(), "");
			}
		}
		return (OpenGLextensions.get(extensionName.toUpperCase()) != null);
	}
}